describe("iD.History", function () {
    var context, history, spy,
        action = function() { return iD.Graph(); };

    beforeEach(function () {
        context = iD();
        history = context.history();
        spy = sinon.spy();
        // clear lock
        context.storage(history._getKey('lock'), null);
    });

    describe("#graph", function () {
        it("returns the current graph", function () {
            expect(history.graph()).to.be.an.instanceOf(iD.Graph);
        });
    });

    describe("#merge", function () {
        it("merges the entities into all graph versions", function () {
            var n = iD.Node({id: 'n'});
            history.merge([n]);
            expect(history.graph().entity('n')).to.equal(n);
        });

        it("emits a change event with the specified extent", function () {
            var extent = {};
            history.on('change', spy);
            history.merge([], extent);
            expect(spy).to.have.been.calledWith(undefined, extent);
        });
    });

    describe("#perform", function () {
        it("returns a difference", function () {
            expect(history.perform(action).changes()).to.eql({});
        });

        it("updates the graph", function () {
            var node = iD.Node();
            history.perform(function (graph) { return graph.replace(node); });
            expect(history.graph().entity(node.id)).to.equal(node);
        });

        it("pushes an undo annotation", function () {
            history.perform(action, "annotation");
            expect(history.undoAnnotation()).to.equal("annotation");
        });

        it("emits a change event", function () {
            history.on('change', spy);
            var difference = history.perform(action);
            expect(spy).to.have.been.calledWith(difference);
        });

        it("performs multiple actions", function () {
            var action1 = sinon.stub().returns(iD.Graph()),
                action2 = sinon.stub().returns(iD.Graph());
            history.perform(action1, action2, "annotation");
            expect(action1).to.have.been.called;
            expect(action2).to.have.been.called;
            expect(history.undoAnnotation()).to.equal("annotation");
        });
    });

    describe("#replace", function () {
        it("returns a difference", function () {
            expect(history.replace(action).changes()).to.eql({});
        });

        it("updates the graph", function () {
            var node = iD.Node();
            history.replace(function (graph) { return graph.replace(node); });
            expect(history.graph().entity(node.id)).to.equal(node);
        });

        it("replaces the undo annotation", function () {
            history.perform(action, "annotation1");
            history.replace(action, "annotation2");
            expect(history.undoAnnotation()).to.equal("annotation2");
        });

        it("emits a change event", function () {
            history.on('change', spy);
            var difference = history.replace(action);
            expect(spy).to.have.been.calledWith(difference);
        });

        it("performs multiple actions", function () {
            var action1 = sinon.stub().returns(iD.Graph()),
                action2 = sinon.stub().returns(iD.Graph());
            history.replace(action1, action2, "annotation");
            expect(action1).to.have.been.called;
            expect(action2).to.have.been.called;
            expect(history.undoAnnotation()).to.equal("annotation");
        });
    });

    describe("#pop", function () {
        it("returns a difference", function () {
            history.perform(action, "annotation");
            expect(history.pop().changes()).to.eql({});
        });

        it("updates the graph", function () {
            history.perform(action, "annotation");
            history.pop();
            expect(history.undoAnnotation()).to.be.undefined;
        });

        it("does not push the redo stack", function () {
            history.perform(action, "annotation");
            history.pop();
            expect(history.redoAnnotation()).to.be.undefined;
        });

        it("emits a change event", function () {
            history.perform(action);
            history.on('change', spy);
            var difference = history.pop();
            expect(spy).to.have.been.calledWith(difference);
        });
    });

    describe("#overwrite", function () {
        it("returns a difference", function () {
            history.perform(action, "annotation");
            expect(history.overwrite(action).changes()).to.eql({});
        });

        it("updates the graph", function () {
            history.perform(action, "annotation");
            var node = iD.Node();
            history.overwrite(function (graph) { return graph.replace(node); });
            expect(history.graph().entity(node.id)).to.equal(node);
        });

        it("replaces the undo annotation", function () {
            history.perform(action, "annotation1");
            history.overwrite(action, "annotation2");
            expect(history.undoAnnotation()).to.equal("annotation2");
        });

        it("does not push the redo stack", function () {
            history.perform(action, "annotation");
            history.overwrite(action, "annotation2");
            expect(history.redoAnnotation()).to.be.undefined;
        });

        it("emits a change event", function () {
            history.perform(action, "annotation");
            history.on('change', spy);
            var difference = history.overwrite(action, "annotation2");
            expect(spy).to.have.been.calledWith(difference);
        });

        it("performs multiple actions", function () {
            var action1 = sinon.stub().returns(iD.Graph()),
                action2 = sinon.stub().returns(iD.Graph());
            history.perform(action, "annotation");
            history.overwrite(action1, action2, "annotation2");
            expect(action1).to.have.been.called;
            expect(action2).to.have.been.called;
            expect(history.undoAnnotation()).to.equal("annotation2");
        });
    });

    describe("#undo", function () {
        it("returns a difference", function () {
            expect(history.undo().changes()).to.eql({});
        });

        it("pops the undo stack", function () {
            history.perform(action, "annotation");
            history.undo();
            expect(history.undoAnnotation()).to.be.undefined;
        });

        it("pushes the redo stack", function () {
            history.perform(action, "annotation");
            history.undo();
            expect(history.redoAnnotation()).to.equal("annotation");
        });

        it("emits an undone event", function () {
            history.perform(action);
            history.on('undone', spy);
            history.undo();
            expect(spy).to.have.been.called;
        });

        it("emits a change event", function () {
            history.perform(action);
            history.on('change', spy);
            var difference = history.undo();
            expect(spy).to.have.been.calledWith(difference);
        });
    });

    describe("#redo", function () {
        it("returns a difference", function () {
            expect(history.redo().changes()).to.eql({});
        });

        it("emits an redone event", function () {
            history.perform(action);
            history.undo();
            history.on('change', spy);
            history.redo();
            expect(spy).to.have.been.called;
        });

        it("emits a change event", function () {
            history.perform(action);
            history.undo();
            history.on('change', spy);
            var difference = history.redo();
            expect(spy).to.have.been.calledWith(difference);
        });
    });

    describe("#changes", function () {
        it("includes created entities", function () {
            var node = iD.Node();
            history.perform(function (graph) { return graph.replace(node); });
            expect(history.changes().created).to.eql([node]);
        });

        it("includes modified entities", function () {
            var node1 = iD.Node({id: "n1"}),
                node2 = node1.update({ tags: { yes: "no" } });
            history.merge([node1]);
            history.perform(function (graph) { return graph.replace(node2); });
            expect(history.changes().modified).to.eql([node2]);
        });

        it("includes deleted entities", function () {
            var node = iD.Node({id: "n1"});
            history.merge([node]);
            history.perform(function (graph) { return graph.remove(node); });
            expect(history.changes().deleted).to.eql([node]);
        });
    });

    describe("#hasChanges", function() {
        it("is true when any of change's values are nonempty", function() {
            var node = iD.Node();
            history.perform(function (graph) { return graph.replace(node); });
            expect(history.hasChanges()).to.eql(true);
        });

        it("is false when all of change's values are empty", function() {
            expect(history.hasChanges()).to.eql(false);
        });
    });

    describe("#reset", function () {
        it("clears the version stack", function () {
            history.perform(action, "annotation");
            history.perform(action, "annotation");
            history.undo();
            history.reset();
            expect(history.undoAnnotation()).to.be.undefined;
            expect(history.redoAnnotation()).to.be.undefined;
        });

        it("emits a change event", function () {
            history.on('change', spy);
            history.reset();
            expect(spy).to.have.been.called;
        });
    });

    describe("#toJSON", function() {
        it("doesn't generate unsaveable changes", function() {
            var node_1 = iD.Node({id: 'n-1'});
            history.perform(iD.actions.AddEntity(node_1));
            history.perform(iD.actions.DeleteNode('n-1'));
            expect(history.toJSON()).to.be.not.ok;
        });

        it("generates v3 JSON", function() {
            var node_1 = iD.Node({id: 'n-1'}),
                node1 = iD.Node({id: 'n1'}),
                node2 = iD.Node({id: 'n2'}),
                node3 = iD.Node({id: 'n3'});
            history.merge([node1, node2, node3]);
            history.perform(iD.actions.AddEntity(node_1)); // addition
            history.perform(iD.actions.ChangeTags('n2', {k: 'v'})); // modification
            history.perform(iD.actions.DeleteNode('n3')); // deletion
            var json = JSON.parse(history.toJSON());
            expect(json.version).to.eql(3);
            expect( _.isEqual(json.entities, [node_1, node2.update({tags: {k: 'v'}})]) ).to.be.ok;
            expect( _.isEqual(json.baseEntities, [node2, node3]) ).to.be.ok;
        });
    });

    describe("#fromJSON", function() {
        it("restores from v1 JSON (creation)", function() {
            var json = {
                "stack": [
                    {"entities": {}},
                    {"entities": {"n-1": {"loc": [1, 2], "id": "n-1"}}, "imageryUsed": ["Bing"], "annotation": "Added a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [1, 2]}));
            expect(history.undoAnnotation()).to.eql("Added a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
        });

        it("restores from v1 JSON (modification)", function() {
            var json = {
                "stack": [
                    {"entities": {}},
                    {"entities": {"n-1": {"loc": [1, 2], "id": "n-1"}}, "imageryUsed": ["Bing"], "annotation": "Added a point."},
                    {"entities": {"n-1": {"loc": [2, 3], "id": "n-1", "v": 1}}, "imageryUsed": ["Bing"], "annotation": "Moved a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 2
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [2, 3], v: 1}));
            expect(history.undoAnnotation()).to.eql("Moved a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
        });

        it("restores from v1 JSON (deletion)", function() {
            var json = {
                "stack": [
                    {"entities": {}},
                    {"entities": {"n1": "undefined"}, "imageryUsed": ["Bing"], "annotation": "Deleted a point."}
                ],
                "nextIDs": {"node": -1, "way": -2, "relation": -3},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            history.merge([iD.Node({id: 'n1'})]);
            expect(history.graph().hasEntity('n1')).to.be.undefined;
            expect(history.undoAnnotation()).to.eql("Deleted a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -1, way: -2, relation: -3});
        });

        it("restores from v2 JSON (creation)", function() {
            var json = {
                "version": 2,
                "entities": [
                    {"loc": [1, 2], "id": "n-1"}
                ],
                "stack": [
                    {},
                    {"modified": ["n-1v0"], "imageryUsed": ["Bing"], "annotation": "Added a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [1, 2]}));
            expect(history.undoAnnotation()).to.eql("Added a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
            expect(history.difference().created().length).to.eql(1);
        });

        it("restores from v2 JSON (modification)", function() {
            var json = {
                "version": 2,
                "entities": [
                    {"loc": [2, 3], "id": "n1", "v": 1}
                ],
                "stack": [
                    {},
                    {"modified": ["n1v1"], "imageryUsed": ["Bing"], "annotation": "Moved a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            history.merge([iD.Node({id: 'n1'})]); // Shouldn't be necessary; flaw in v2 format (see #2135)
            expect(history.graph().entity('n1')).to.eql(iD.Node({id: 'n1', loc: [2, 3], v: 1}));
            expect(history.undoAnnotation()).to.eql("Moved a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
            expect(history.difference().modified().length).to.eql(1);
        });

        it("restores from v2 JSON (deletion)", function() {
            var json = {
                "version": 2,
                "entities": [],
                "stack": [
                    {},
                    {"deleted": ["n1"], "imageryUsed": ["Bing"], "annotation": "Deleted a point."}
                ],
                "nextIDs": {"node": -1, "way": -2, "relation": -3},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            history.merge([iD.Node({id: 'n1'})]); // Shouldn't be necessary; flaw in v2 format (see #2135)
            expect(history.graph().hasEntity('n1')).to.be.undefined;
            expect(history.undoAnnotation()).to.eql("Deleted a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -1, way: -2, relation: -3});
            expect(history.difference().deleted().length).to.eql(1);
        });

        it("restores from v3 JSON (creation)", function() {
            var json = {
                "version": 3,
                "entities": [
                    {"loc": [1, 2], "id": "n-1"}
                ],
                "baseEntities": [],
                "stack": [
                    {},
                    {"modified": ["n-1v0"], "imageryUsed": ["Bing"], "annotation": "Added a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [1, 2]}));
            expect(history.undoAnnotation()).to.eql("Added a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
            expect(history.difference().created().length).to.eql(1);
        });

        it("restores from v3 JSON (modification)", function() {
            var json = {
                "version": 3,
                "entities": [
                    {"loc": [2, 3], "id": "n1", "v": 1}
                ],
                "baseEntities": [{"loc": [1, 2], "id": "n1"}],
                "stack": [
                    {},
                    {"modified": ["n1v1"], "imageryUsed": ["Bing"], "annotation": "Moved a point."}
                ],
                "nextIDs": {"node": -2, "way": -1, "relation": -1},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().entity('n1')).to.eql(iD.Node({id: 'n1', loc: [2, 3], v: 1}));
            expect(history.undoAnnotation()).to.eql("Moved a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1});
            expect(history.difference().modified().length).to.eql(1);
        });

        it("restores from v3 JSON (deletion)", function() {
            var json = {
                "version": 3,
                "entities": [],
                "baseEntities": [{"loc": [1, 2], "id": "n1"}],
                "stack": [
                    {},
                    {"deleted": ["n1"], "imageryUsed": ["Bing"], "annotation": "Deleted a point."}
                ],
                "nextIDs": {"node": -1, "way": -2, "relation": -3},
                "index": 1
            };
            history.fromJSON(JSON.stringify(json));
            expect(history.graph().hasEntity('n1')).to.be.undefined;
            expect(history.undoAnnotation()).to.eql("Deleted a point.");
            expect(history.imageryUsed()).to.eql(["Bing"]);
            expect(iD.Entity.id.next).to.eql({node: -1, way: -2, relation: -3});
            expect(history.difference().deleted().length).to.eql(1);
        });
    });
});
